Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimizations to improve performance on complex conditional schemas #2466

Closed
wants to merge 24 commits into from

Conversation

nickgros
Copy link
Contributor

@nickgros nickgros commented Jul 12, 2021

I've picked up from where #1666 has left off, hoping that the work I've done so far will be enough to add this feature.

In addition to the changes outlined in #1666, I've tried to address changes requested in that review, and fix a few other issues as well.

Here's a link to just the changes that I've personally made since I branched from the work in #1666

Since conditional support was added in #2700, this branch only exists to preserve the optimizations I've done to improve performance in complex schemas. We struggle with performance on complex conditional schemas because we re-run validation on every input change to determine if the condition has changed. Presently, I've worked around this with lots of memoization and attempts to get ajv to use its internal cache, but we could probably do this better/smarter than what I've done so far.

Checklist

  • I'm updating documentation
  • I'm adding or updating code
    • I've added and/or updated tests
    • I've updated docs if needed
  • I'm adding a new feature
    • I've updated the playground with an example use of the feature

Saulzi and others added 7 commits June 23, 2021 09:20
Fixed issue with arrays
Removed only from tests

better allOf support

better allOf support

mend

If Then Else , Allof

Fixed typo and included .lock.json

added support for  in if/then/else

Added support for const as per rjsf-team#1627

Moved handling of if then / else from SchemaField to utils and tidied up things
Comment on lines -312 to -314
} finally {
// make sure we remove the rootSchema from the global ajv instance
ajv.removeSchema(ROOT_SCHEMA_PREFIX);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the root schema being removed (before my change)?

Since we now need to run the validator frequently to evaluate conditional subschemas, keeping the root schema in the cache is essential for performance, especially for large schemas.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I've figured it out: this function uses the same ajv instance as validateFormData, which has some advantages and some drawbacks. I think with this new implementation (not removing the root schema), validation breaks for certain schemas. I'm not happy with the performance of the current implementation, though, so I'll have to figure something else out.

Copy link
Contributor Author

@nickgros nickgros Jul 14, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One cause of performance issues here is that we can't take advantage of AJV's cache because withIdRefPrefix creates a new object every time. We see a substantial improvement in performance if we memoize withIdRefPrefix, because validate sees the same schema and can use the ajv cache instead of recompiling. I'm not yet sure that memoization is 100% safe here, but it seems like a possible path forward.

Another thing I've noticed: we get different behavior with the current implementation if the root schema has an $id. When an $id is present, I think AJV totally ignores the root schema prefix when we call addSchema(rootSchema, ROOT_SCHEMA_PREFIX), so the removeSchema call never works. Not 100% on this either, though.

Copy link
Contributor Author

@nickgros nickgros Jul 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for $id issues, I've filed #2471, ideally would like to address it here (at the risk of increasing scope of this PR) since how RJSF uses AJV is closely related with adding if/then/else support

});
});

it("handles nested if then else", () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#1666 didn't handle this case properly. A small change in the resolveSchema function fixed it (changing if "if" in schema to while "if" in schema), and here's a test to prove it.

expect(node.querySelector("select[id=root_city]")).not.eql(null);
});

it("handles additionalProperties with if then else", () => {
Copy link
Contributor Author

@nickgros nickgros Jul 12, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests that we fixed the issue in this comment: #1666 (comment)

@nickgros nickgros changed the title Implementation of If Then Else Implementation of If Then Else, better support for const Jul 12, 2021
@nickgros
Copy link
Contributor Author

Converting this PR to a draft because while I think my changes in 78fc62c (#2466) are a step forward, I think it's going to break some things and I don't want to indicate that this should be merged as-is without more tests (in addition to fixing the ones that fail).

@nickgros nickgros marked this pull request as draft July 15, 2021 18:22
@jimmycallin
Copy link
Collaborator

I tried it out a bit and I think this looks great! If we can get it in a mergeable state where only the if-then-else changes are included in this PR, I think we are pretty close.

A few comments:

  • Currently if you type a value in a conditional field, and then change the parent such that the field is removed, the value is still included in formData and will be submitted, unless additionalProperties: false is included. Is this the preferred behaviour? I can see pros and cons of both resetting the value of the formData and including it, but I'm leaning towards resetting the values.
  • The memoization changes seem to cause issues when omitExtraData is set to true - I'm getting "maximum call size exceeded". See attached screenshot.
    image

import fill from "core-js-pure/features/array/fill";
import union from "lodash/union";
import jsonpointer from "jsonpointer";
import fields from "./components/fields";
import widgets from "./components/widgets";
import validateFormData, { isValid } from "./validate";
import _ from "lodash";
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep the build size down, it's preferable to import just the necessary function similarly to line 4.

Suggested change
import _ from "lodash";
import memoize from "lodash/memoize";

if (schema.hasOwnProperty("$ref")) {
return resolveReference(schema, rootSchema, formData);
} else if (schema.hasOwnProperty("dependencies")) {
const resolvedSchema = resolveDependencies(schema, rootSchema, formData);
return retrieveSchema(resolvedSchema, rootSchema, formData);
} else if (schema.hasOwnProperty("allOf")) {
} else if (schema["allOf"]) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the reason for this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allOf may be set to undefined here: https://github.com/nickgros/react-jsonschema-form/blob/5e7a08dce80c0c1c85293ac3465c63d65693c6d4/packages/core/src/utils.js#L767

I think I changed that per a comment on the previous PR. Could use delete instead and revert this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switching that line to delete actually fixed the liveOmit issue that I mentioned here. I'm not sure if it fixed the stack overflow error that you saw.

docs/index.md Outdated

For arrays this is not the case. Defining an array, when a parent also defines an array, will be overwritten. This is only true when arrays are used in the same level, for objects within these arrays, they will be deeply merged again.

## Tips and tricks
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what's happening here with these docs changes, they are unrelated to the PR and are also out commented. I guess this should be reverted?

@@ -249,7 +249,8 @@ function SchemaFieldRender(props) {
const FieldTemplate =
uiSchema["ui:FieldTemplate"] || registry.FieldTemplate || DefaultTemplate;
let idSchema = props.idSchema;
const schema = retrieveSchema(props.schema, rootSchema, formData);
var schema = retrieveSchema(props.schema, rootSchema, formData);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm assuming all const -> var related changes are due to the original PR being a bit old. In any case, const is highly preferable to var, so please revert all of these changes.

@nickgros
Copy link
Contributor Author

nickgros commented Jul 22, 2021

Thanks for taking a look @jimmycallin!

Currently if you type a value in a conditional field, and then change the parent such that the field is removed, the value is still included in formData and will be submitted, unless additionalProperties: false is included. Is this the preferred behaviour? I can see pros and cons of both resetting the value of the formData and including it, but I'm leaning towards resetting the values.

For my use case, I need the option to keep data as an additional property if its field is removed. As our users fill out the form, we want to be as nondestructive as possible with their data, and give them the opportunity to 'backtrack' without having to re-fill fields that they may have already completed.

Shouldn't this concern be handled by omitExtraData and liveOmit? It seems like liveOmit is working properly (except for your concern) , but nothing is being omitted on submit if liveOmit is false, so that will need to be fixed (I'm actually seeing this issue in the current playground version 😬) . I'm also not actually seeing a difference in behavior when I change additionalProperties like you note.

The memoization changes seem to cause issues when omitExtraData is set to true - I'm getting "maximum call size exceeded". See attached screenshot.

Good catch, and thanks for attaching the stack trace! I'm having trouble reproducing this, but you guided me to find that I can't actually change the data in the "All Of If Then Else" example if liveOmit is true. Possible that these are related issues.

Edit: I realize now that unless liveOmit is enabled, omitExtraData applies only to the submitted data and not the formData.

@nickgros nickgros changed the title Implementation of If Then Else, better support for const Implementation of If Then Else Jul 22, 2021
@nickgros nickgros marked this pull request as ready for review July 22, 2021 14:47
@jeremypele
Copy link

jeremypele commented Aug 4, 2021

Thanks for that PR @nickgros 🎉

Is there anything I can help you with to be able to complete this feature? Would love to this merged sooner than later

Added if then else logic to resolve schemas
@Juansasa Juansasa mentioned this pull request Aug 6, 2021
7 tasks
@epicfaace epicfaace self-requested a review August 11, 2021 14:30
@dieseldjango
Copy link
Contributor

I need support for if-then-else and it's great to see there's a PR, but appears stalled. Anything I can help with?

@nickgros
Copy link
Contributor Author

I don't yet want to close this because I think there's still some value in my changes, but I think #2506 is a better foundation. I'll see if I can integrate those changes into this PR (and in doing so, see if there's a meaningful difference in behavior between how these handle if/then/else)

nickgros added a commit to nickgros/react-jsonschema-form that referenced this pull request Feb 10, 2022
epicfaace pushed a commit that referenced this pull request Feb 18, 2022
* Added tests
Added if then else logic to resolve schemas

* Changed resolve method's name

* Added $ref tests

* Code review changes

* Remove only on test, add integration tests from #2466

* Fixed merge order

* Update tests method names

* Code review changes #2700

Co-authored-by: Juansasa <[email protected]>
Co-authored-by: Quang Vu <[email protected]>
@nickgros nickgros marked this pull request as draft February 20, 2022 13:39
@nickgros nickgros changed the title Implementation of If Then Else Optimizations to improve performance on complex conditional schemas Feb 20, 2022
@heath-freenome
Copy link
Member

@nickgros Does it make sense to rethink these changes on top of the v5 beta's @rjsf/validator-ajv8?

@heath-freenome heath-freenome added awaiting response v5 refactor Needs refactor due to v5 breaking changes labels Sep 7, 2022
@nickgros
Copy link
Contributor Author

This is probably not the right way to optimize validation. Closing

@nickgros nickgros closed this Feb 10, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting response v5 refactor Needs refactor due to v5 breaking changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants